PERF: Optimize read_excel nrows
State: closed

- [x] closes #32727
- [x] [Tests added and passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#writing-tests) if fixing a bug or adding a new feature
- [x] All [code checks passed](https://pandas.pydata.org/pandas-docs/dev/development/contributing_codebase.html#pre-commit).
- [x] Added an entry in the latest `doc/source/whatsnew/vX.X.X.rst` file if fixing a bug or adding a new feature.

I timed read_excel on a file with 10 columns and 1000 rows, and report the best of 10 repeats (ms) below. This change can make a modest improvement on xls and ods files, and a significant improvement on xlsx and xlsb files. When `nrows=None` this has no measurable impact on the run time.

| ext  | nrows | time (main) | time (this branch) |
|------|-------|------------|------------|
| xls  | None  |    22.4    |    22.1    |
| xls  | 10    |    21.4    |    17.0    |
| xlsx | None  |    99.1    |    99.7    |
| xlsx | 10    |    98.0    |     8.8    |
| xlsb | None  |    81.0    |    80.2    |
| xlsb | 10    |    80.2    |     4.8    |
| ods  | None  |   571      |   569      |
| ods  | 10    |   566      |   517      |

Here are the results of ```asv run -e -E existing --bench ReadExcel``` showing similar results (the benchmark spreadsheet is different than the one above).

```
[75.00%] ··· io.excel.ReadExcel.time_read_excel                              ok
[75.00%] ··· ========== ============
               engine               
             ---------- ------------
                xlrd     36.5±0.3ms 
              openpyxl   162±0.1ms  
                odf       688±5ms   
             ========== ============

[100.00%] ··· io.excel.ReadExcelNRows.time_read_excel                         ok
[100.00%] ··· ========== ============
                engine               
              ---------- ------------
                 xlrd     24.5±0.1ms 
               openpyxl   29.0±0.1ms 
                 odf       508±3ms   
              ========== ============
```

Labels:
name: Performance
description: Memory or execution speed performance
name: IO Excel
description: read_excel, to_excel

Comments:
> Nice gain, especially in xlsx (I think that's pretty common). Do you know why you don't see similar gains in old/xls?

Thanks! In the case of xls, xlrd parses all of the data on first read, so there's no saving in I/O time, just a small saving inside get_sheet_data. On the other hand, I was surprised that ods showed so little improvement, especially since ods and xlsx are both zip files containing xml, as far as I understand. I'm also not sure why ods is so much slower than xlsx in the first place.

Thanks for the great recommendations on this PR. I'll take a look at them in the coming days.
@rhshadrach I've made the suggested changes and they were all huge improvements except maybe the type hints. I have one failing mypy error that I'm not sure how to fix. Also, in my last commit I added a bunch of `assert isinstance` statements to work around the fact that mypy doesn't understand the type narrowing of e.g. `is_integer`. There were a few similar assert statements in _base.py, but I'm not convinced that my most recent commit was an improvement. It seems like I've lost readability, conciseness, and the original benefit of `is_integer` versus `isinstance(int)`. Maybe I should use type hint `Any` in validate_header_arg? I'm open to any suggestions you have. (No rush at all.) Have a great weekend.
@rhshadrach I think this is ready for review. It didn't pass all tests, but failures were different the last two times, so I suspect that these failures aren't from this PR. Of course, let me know if I'm wrong about that.
thanks @ahawryluk