- You always terminate the reading of the row if the element isn't found. This means you can have just one Try-Catch block that wraps your If statement. 
 - Instead of checking for `&&` in your first statement, use `||` (or instead of and). You don't really care if `ReadUntilCellError` is set at that point; only `ReadUntilEmpty` matters. This eliminates an entire block from the If statement. 
 - You could go one step further and only check the flag just before, or with, the null value check.

        public IEnumerable<string> ReadRow(string spreadsheet, int column, int row, ReadOptions readOptions = ReadOptions.ReadUntilEmpty)
        {
        var values = new List<string>();

        try
        {
            for (var currentColumn = column; ; currentColumn++)
            {
                var value = ReadCell(spreadsheet, currentColumn, row);
            
                if readOptions.HasFlag(ReadOptions.ReadUntilCellError))
                {
                    // Cell has errors, so we exit from the loop
                    if (value == null) break;
                }
        
                values.Add(value);
            }
        }
        catch (ElementNotFoundException)
        {
            // The row is terminated
            // so we exit the loop withouth loggin any 
        }

        return values;
        }

Ultimately I realized there's never actually a need to check the `ReadUntilError` flag, so you could probably just drop the enem and change `ReadUntilEmpty` to a Boolean with a default value of `false`.